/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nageoffer.shortlink.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.admin.common.biz.user.UserContext;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.admin.common.convention.result.Result;
import com.nageoffer.shortlink.admin.dao.entity.GroupDO;
import com.nageoffer.shortlink.admin.dao.entity.GroupUniqueDO;
import com.nageoffer.shortlink.admin.dao.mapper.GroupMapper;
import com.nageoffer.shortlink.admin.dao.mapper.GroupUniqueMapper;
import com.nageoffer.shortlink.admin.dto.req.ShortLinkGroupSortReqDTO;
import com.nageoffer.shortlink.admin.dto.req.ShortLinkGroupUpdateReqDTO;
import com.nageoffer.shortlink.admin.dto.resp.ShortLinkGroupRespDTO;
import com.nageoffer.shortlink.admin.remote.ShortLinkActualRemoteService;
import com.nageoffer.shortlink.admin.remote.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.nageoffer.shortlink.admin.service.GroupService;
import com.nageoffer.shortlink.admin.toolkit.RandomGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static com.nageoffer.shortlink.admin.common.constant.RedisCacheConstant.LOCK_GROUP_CREATE_KEY;

/**
 * 短链接分组接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupServiceImpl extends ServiceImpl<GroupMapper, GroupDO> implements GroupService {

    private final RBloomFilter<String> gidRegisterCachePenetrationBloomFilter;
    private final GroupUniqueMapper groupUniqueMapper;
    private final ShortLinkActualRemoteService shortLinkActualRemoteService;
    private final RedissonClient redissonClient;

    @Value("${short-link.group.max-num}")
    private Integer groupMaxNum;

    /**
     * 保存指定名称的用户组信息（当前登录用户上下文）
     * <p>
     * 该方法通过用户上下文自动获取当前登录用户名，将其与给定组名组合进行保存。
     * 实际保存操作委托给重载方法处理。
     *
     * @param groupName 需要保存的用户组名称，应符合系统命名规范（非空，唯一性）
     *
     * @override 实现父类/接口定义的组保存方法
     */
    @Override
    public void saveGroup(String groupName) {
        // 从用户上下文获取当前用户名，组合调用底层保存逻辑
        saveGroup(UserContext.getUsername(), groupName);
    }


    /**
     * 保存用户分组信息
     *
     * 此方法用于在数据库中创建一个新的用户分组为了确保数据一致性，它使用了Redis分布式锁来防止并发创建导致的问题
     * 锁的粒度是用户级别的，意味着同一时间只有一个操作可以为特定用户创建分组
     *
     * @param username 用户名，表示要创建分组的用户
     * @param groupName 分组名称，用户为分组指定的名称
     */
    @Override
    public void saveGroup(String username, String groupName) {
        // 获取Redis分布式锁，防止并发创建分组
        RLock lock = redissonClient.getLock(String.format(LOCK_GROUP_CREATE_KEY, username));
        lock.lock();
        try {
            // 查询用户已有的分组数量，以检查是否超过了最大分组数
            LambdaQueryWrapper<GroupDO> queryWrapper = Wrappers.lambdaQuery(GroupDO.class)
                    .eq(GroupDO::getUsername, username)
                    .eq(GroupDO::getDelFlag, 0);
            List<GroupDO> groupDOList = baseMapper.selectList(queryWrapper);
            if (CollUtil.isNotEmpty(groupDOList) && groupDOList.size() == groupMaxNum) {
                throw new ClientException(String.format("已超出最大分组数：%d", groupMaxNum));
            }

            // 初始化重试计数和最大重试次数，用于生成分组标识（gid）
            int retryCount = 0;
            int maxRetries = 10;
            String gid = null;
            while (retryCount < maxRetries) {
                gid = saveGroupUniqueReturnGid();
                if (StrUtil.isNotEmpty(gid)) {
                    // 当成功生成gid后，创建GroupDO对象并插入数据库，然后将gid添加到缓存和布隆过滤器中
                    GroupDO groupDO = GroupDO.builder()
                            .gid(gid)
                            .sortOrder(0)
                            .username(username)
                            .name(groupName)
                            .build();
                    baseMapper.insert(groupDO);
                    gidRegisterCachePenetrationBloomFilter.add(gid);
                    break;
                }
                retryCount++;
            }

            // 如果经过多次重试仍然无法生成gid，则抛出异常
            if (StrUtil.isEmpty(gid)) {
                throw new ServiceException("生成分组标识频繁");
            }
        } finally {
            // 释放锁，确保其他等待的操作可以继续执行
            lock.unlock();
        }
    }

    /**
     * 获取当前用户短链接组列表
     *
     * 此方法查询并返回属于当前用户的所有短链接组信息，包括每个组的基本信息和短链接数量
     * 它首先构建一个查询条件，以确保只获取未删除且属于当前用户的组，然后按排序顺序和更新时间降序排列
     * 接着，调用远程服务获取每个组的短链接数量，并将组信息与短链接数量合并，最后返回合并后的列表
     *
     * @return 当前用户的短链接组列表，包含每个组的短链接数量
     */
    @Override
    public List<ShortLinkGroupRespDTO> listGroup() {
        // 构建查询条件，查询未删除且属于当前用户的组，并按排序顺序和更新时间降序排列
        LambdaQueryWrapper<GroupDO> queryWrapper = Wrappers.lambdaQuery(GroupDO.class)
                .eq(GroupDO::getDelFlag, 0)
                .eq(GroupDO::getUsername, UserContext.getUsername())
                .orderByDesc(GroupDO::getSortOrder, GroupDO::getUpdateTime);
        List<GroupDO> groupDOList = baseMapper.selectList(queryWrapper);

        // 调用远程服务，获取每个组的短链接数量
        Result<List<ShortLinkGroupCountQueryRespDTO>> listResult = shortLinkActualRemoteService
                .listGroupShortLinkCount(groupDOList.stream().map(GroupDO::getGid).toList());

        // 将组信息转换为响应DTO列表
        List<ShortLinkGroupRespDTO> shortLinkGroupRespDTOList = BeanUtil.copyToList(groupDOList, ShortLinkGroupRespDTO.class);

        // 为每个组设置短链接数量
        shortLinkGroupRespDTOList.forEach(each -> {
            Optional<ShortLinkGroupCountQueryRespDTO> first = listResult.getData().stream()
                    .filter(item -> Objects.equals(item.getGid(), each.getGid()))
                    .findFirst();
            first.ifPresent(item -> each.setShortLinkCount(first.get().getShortLinkCount()));
        });

        // 返回合并后的组列表
        return shortLinkGroupRespDTOList;
    }

    /**
     * 更新短链接分组信息
     * 此方法根据用户请求参数更新特定短链接分组的信息
     * 主要功能包括：验证分组属于当前用户、分组未被删除，然后更新分组名称
     *
     * @param requestParam 包含要更新的分组信息的请求参数对象
     */
    @Override
    public void updateGroup(ShortLinkGroupUpdateReqDTO requestParam) {
        // 创建更新条件包装器，确保只更新当前用户、指定ID且未被删除的分组
        LambdaUpdateWrapper<GroupDO> updateWrapper = Wrappers.lambdaUpdate(GroupDO.class)
                .eq(GroupDO::getUsername, UserContext.getUsername())
                .eq(GroupDO::getGid, requestParam.getGid())
                .eq(GroupDO::getDelFlag, 0);

        // 创建GroupDO对象并设置新的分组名称
        GroupDO groupDO = new GroupDO();
        groupDO.setName(requestParam.getName());

        // 执行更新操作，根据条件和新名称更新数据库中的分组信息
        baseMapper.update(groupDO, updateWrapper);
    }

    /**
     * 删除指定的用户组
     * 通过设置用户组的删除标志位来实现逻辑删除
     *
     * @param gid 用户组ID，用于标识要删除的用户组
     */
    @Override
    public void deleteGroup(String gid) {
        // 创建更新条件构造器，用于指定删除操作的作用范围
        LambdaUpdateWrapper<GroupDO> updateWrapper = Wrappers.lambdaUpdate(GroupDO.class)
                .eq(GroupDO::getUsername, UserContext.getUsername()) // 当前用户，确保操作权限
                .eq(GroupDO::getGid, gid) // 指定的用户组ID
                .eq(GroupDO::getDelFlag, 0); // 未删除的状态
        // 创建一个新的用户组数据对象，用于设置删除标志位
        GroupDO groupDO = new GroupDO();
        groupDO.setDelFlag(1); // 设置删除标志位为1，表示该用户组已被删除
        // 执行更新操作，逻辑删除指定的用户组
        baseMapper.update(groupDO, updateWrapper);
    }

    /**
     * 重写排序分组方法
     * 根据用户请求参数更新数据库中每个分组的排序顺序
     * 此方法遍历用户请求参数列表，为每个分组构建更新条件，并执行更新操作
     *
     * @param requestParam 包含分组排序请求信息的列表，每个元素代表一个分组及其新的排序顺序
     */
    @Override
    public void sortGroup(List<ShortLinkGroupSortReqDTO> requestParam) {
        // 遍历请求参数列表，对每个分组进行排序更新
        requestParam.forEach(each -> {
            // 构建GroupDO对象，仅设置需要更新的排序字段
            GroupDO groupDO = GroupDO.builder()
                    .sortOrder(each.getSortOrder())
                    .build();

            // 构建更新条件：根据用户名、分组ID和删除标志来定位需要更新的分组
            LambdaUpdateWrapper<GroupDO> updateWrapper = Wrappers.lambdaUpdate(GroupDO.class)
                    .eq(GroupDO::getUsername, UserContext.getUsername())
                    .eq(GroupDO::getGid, each.getGid())
                    .eq(GroupDO::getDelFlag, 0);

            // 执行更新操作，将新的排序值更新到数据库中对应的分组记录
            baseMapper.update(groupDO, updateWrapper);
        });
    }

    /**
     * 生成并保存群组唯一标识符（GID）
     *
     * 此方法用于生成一个唯一的群组标识符，并将其保存到数据库中
     * 如果生成的GID已存在（通过缓存过滤器判断），或数据库插入时发现重复键，
     * 则返回null，表示未能成功保存新的群组唯一标识符
     *
     * @return 成功保存的群组唯一标识符（GID），如果未能成功保存则返回null
     */
    private String saveGroupUniqueReturnGid() {
        // 生成一个随机的群组唯一标识符
        String gid = RandomGenerator.generateRandom();

        // 检查生成的GID是否已存在于缓存的布隆过滤器中
        if (gidRegisterCachePenetrationBloomFilter.contains(gid)) {
            // 如果存在，返回null，表示无法生成新的唯一GID
            return null;
        }

        // 创建一个GroupUniqueDO对象，用于存储生成的GID
        GroupUniqueDO groupUniqueDO = GroupUniqueDO.builder()
                .gid(gid)
                .build();

        // 尝试将生成的GID插入数据库
        try {
            groupUniqueMapper.insert(groupUniqueDO);
        } catch (DuplicateKeyException e) {
            // 如果插入时发生重复键异常，返回null，表示未能成功保存GID
            return null;
        }

        // 成功插入数据库后，返回生成的GID
        return gid;
    }
}
